using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Assignment3.Objects3D;

namespace Assignment3
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SpriteFont font;

        private Matrix world;
        private Matrix view;
        private Matrix projection;

        private Texture2D[] stageTextures;

        private Vector3 characterPositionDefault;
        private Vector3 characterCameraDefault;

        private float fovAngle;
        private float zoomFactor;
        private float aspectRatio;
        private float nearPlane;//near clipping plane distance
        private float farPlane;//far clipping plane distance

        private Character character;

        private Enemy enemy;
        private Model zombie;
        private float enemySpeed;
        private int enemyX, enemyZ;
        private int direction;
        private Random enemyRandom;

        private float movingSpeed;
        private float upDownArc;

        private Quaternion charRotation = Quaternion.Identity;
        private Quaternion camRotation = Quaternion.Identity;

        private BasicEffect effect;

        private bool isWallPenetrate = false;


        private MazeGenerator maze;

        private List<WallBlock> walls;
        private Vector3 wallSize;
        private Vector3 wallPos;

        private List<FloorTile> floorTiles;
        private Vector3 tileSize;
        private Vector3 tilePos;

        private BoundingBox playableArea;

        bool isWalking;
        SoundEffect playerSound;
        SoundEffectInstance playerSoundInstance;
        SoundEffect collideSound;
        SoundEffect daySound;
        SoundEffectInstance daySoundInstance;
        SoundEffect nightSound;
        SoundEffectInstance nightSoundInstance;
        float soundVolume;
        SoundEffect zombieSound;
        SoundEffectInstance zombieSoundInstance;
        float distanceVolume;
        bool isSilence;

        private InputProcessor input;

        static readonly string[] textures =
        {
            "Textures/Wall1",
            "Textures/Wall2",
            "Textures/Wall3",
            "Textures/Wall4",
            "Textures/Wall1",
            "Textures/Wall1",
            "Textures/Floor",
        };

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            input = new InputProcessor(this);
            Components.Add(input);

            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;

            //set up size for wall blocks
            wallSize = new Vector3(4, 6, 4);
            tileSize = wallSize;
            zoomFactor = 0;

            //set up value for projection matrix
            fovAngle = MathHelper.ToRadians(45);
            aspectRatio = graphics.PreferredBackBufferWidth / graphics.PreferredBackBufferHeight;
            nearPlane = 0.4f; // the near clipping plane distance
            farPlane = 10000f; // the far clipping plane distance

            //set up initial position for character
            characterPositionDefault = new Vector3(wallSize.X * 1.5f, wallSize.Y * 0.5f, -(wallSize.Z * 1.5f));
            characterCameraDefault = new Vector3(0f, 0f, 0.01f);
            character = new Character(characterPositionDefault, characterCameraDefault);

            enemySpeed = .05f;
            direction = 0;
            enemyRandom = new Random();

            isWalking = false;
            isSilence = false;

            stageTextures = new Texture2D[7];
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }
        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
            font = Content.Load<SpriteFont>("font");

            world = Matrix.CreateTranslation(0, 0, 0);

            effect = new BasicEffect(GraphicsDevice);
            effect.TextureEnabled = true;

            //set up fog
            effect.FogColor = Color.White.ToVector3();
            effect.FogStart = 0.5f;
            effect.FogEnd = 20f;
            effect.FogEnabled = false;

            //set up flash light
            effect.DirectionalLight0.Enabled = true;
            effect.DirectionalLight0.DiffuseColor = new Vector3(1f, 1f, 0f);
            effect.DirectionalLight0.SpecularColor = new Vector3(1f, 1f, 1f);
            effect.DirectionalLight1.Enabled = false;
            effect.DirectionalLight2.Enabled = false;
            effect.SpecularPower = 20;
            effect.AmbientLightColor = Vector3.Zero;
            effect.EmissiveColor = Vector3.Zero;


            maze = new MazeGenerator();
            playableArea = new BoundingBox(new Vector3(wallSize.X, 0f, -wallSize.Z), new Vector3(wallSize.X * (maze.Columm - 1), wallSize.Y, -wallSize.Z * (maze.Row - 1)));

            zombie = Content.Load<Model>("Model/Zombie");

            enemyX = maze.Columm - 2;
            enemyZ = maze.Row - 2;
            enemy = new Enemy(new Vector3((enemyX * wallSize.X + wallSize.X / 2), 0f, -(enemyZ * wallSize.Z + wallSize.Z / 2)), new Vector3(.05f, .08f, .08f));

            walls = new List<WallBlock>();

            floorTiles = new List<FloorTile>();

            int i = 0;
            for (i = 0; i < textures.Length; i++)
            {
                stageTextures[i] = Content.Load<Texture2D>(textures[i]);
            }

            //setting up walls' positions and floor tiles' positions according to the maze's matrix
            for (i = 0; i < maze.Row; i++)
            {
                for (int j = 0; j < maze.Row; j++)
                {
                    if (maze.Maze[i, j])
                    {
                        wallPos = new Vector3(i * wallSize.X, 0, -j * wallSize.Z);
                        walls.Add(new WallBlock(wallSize, wallPos));
                    }
                    else
                    {
                        tilePos = new Vector3(i * wallSize.X, 0, -j * wallSize.Z);
                        floorTiles.Add(new FloorTile(tileSize, tilePos));
                    }
                }
            }

            playerSound = Content.Load<SoundEffect>("Sound/FootStep");
            playerSoundInstance = playerSound.CreateInstance();
            playerSoundInstance.IsLooped = true;
            collideSound = Content.Load<SoundEffect>("Sound/CollideSound");
            daySound = Content.Load<SoundEffect>("Sound/DaySound");
            nightSound = Content.Load<SoundEffect>("Sound/NightSound");
            zombieSound = Content.Load<SoundEffect>("Sound/ZombieSound");
            daySoundInstance = daySound.CreateInstance();
            nightSoundInstance = nightSound.CreateInstance();
            zombieSoundInstance = zombieSound.CreateInstance();
            daySoundInstance.IsLooped = true;
            nightSoundInstance.IsLooped = true;
            zombieSoundInstance.IsLooped = true;

            soundVolume = daySoundInstance.Volume;
            nightSoundInstance.Volume = soundVolume;

            distanceVolume = .5f;
            zombieSoundInstance.Volume = distanceVolume;
            zombieSoundInstance.Play();

            //daySoundInstance.Play();
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || input.isKeyPressed(Keys.Escape))
                this.Exit();

            float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds * 0.002f;

            fovAngle = MathHelper.ToRadians(45 + zoomFactor);

            Reset();
            //check to toggle wall penetrate
            if (input.isButtonPressed(Buttons.Y, PlayerIndex.One) || input.isKeyPressed(Keys.Q))
                isWallPenetrate = !isWallPenetrate;

            silence();            
            Move(time);
            RotateLeftRight(time);
            FlashLight();
            DayNightToggle();
            DoFog();
            DoZoom();            

            UpdateCamera();
            RotateUpDown(time);

            distanceVolume = (float) (10/Math.Sqrt(Math.Pow((character.charPos.X - enemy.position.X), 2f) + Math.Pow((character.charPos.Z - enemy.position.Z), 2)));
            if (distanceVolume > 1)
                zombieSoundInstance.Volume = 1;
            else
                zombieSoundInstance.Volume = distanceVolume;
            //Console.WriteLine(distanceVolume);

            //checking enemy's direction 0 = down, 1 = left, 2 = up, 3 = right
            switch (direction)
            {
                //moving down
                case 0:
                    //check the next position if false => no wall
                    if (!maze.Maze[enemyX, enemyZ - 1])
                    {
                        enemy.position.Z += enemySpeed;
                        //if enemy reached the position then change to the next
                        if ((-enemyZ * wallSize.Z) < enemy.position.Z)
                        {
                            if (enemyZ > 1)
                            {
                                enemyZ = enemyZ - 1;
                            }
                        }
                    }
                    else
                    {
                        //if cannot move down anymore, move the enemy to the correct position and random new direction
                        enemy.position.Z = -(enemyZ * wallSize.Z + wallSize.Z / 2);
                        direction = enemyRandom.Next(4);
                    }
                    break;
                //moving left, up and right follow the same sequence as moving down
                case 1:
                    if (!maze.Maze[enemyX - 1, enemyZ])
                    {
                        enemy.position.X -= enemySpeed;
                        if (enemyX * wallSize.X > enemy.position.X)
                        {
                            if (enemyX > 1)
                            {
                                enemyX = enemyX - 1;
                            }
                        }
                    }
                    else
                    {
                        enemy.position.X = (enemyX * wallSize.X + wallSize.X / 2);
                        direction = enemyRandom.Next(4);
                    }
                    break;
                case 2:
                    if (!maze.Maze[enemyX, enemyZ + 1])
                    {
                        enemy.position.Z -= enemySpeed;
                        if ((-enemyZ * wallSize.Z) > enemy.position.Z)
                        {
                            if (enemyZ < maze.Row - 2)
                            {
                                enemyZ = enemyZ + 1;
                            }
                        }
                    }
                    else
                    {
                        enemy.position.Z = -(enemyZ * wallSize.Z + wallSize.Z / 2);
                        direction = enemyRandom.Next(4);
                    }
                    break;
                case 3:
                    if (!maze.Maze[enemyX + 1, enemyZ])
                    {
                        enemy.position.X += enemySpeed;
                        if (enemyX * wallSize.X < enemy.position.X)
                        {
                            if (enemyX < maze.Columm - 2)
                            {
                                enemyX = enemyX + 1;
                            }
                        }
                    }
                    else
                    {
                        enemy.position.X = (enemyX * wallSize.X + wallSize.X / 2);
                        direction = enemyRandom.Next(4);
                    }
                    break;
                default:
                    break;
            }

            effect.World = world;
            effect.View = view * Matrix.CreateRotationX(upDownArc);
            effect.Projection = projection;


            base.Update(gameTime);
        }

        private void FlashLight()
        {
            effect.DirectionalLight0.Direction = Vector3.Transform(Vector3.Forward, charRotation);
            if (input.isKeyPressed(Keys.F) || input.isDpadUpPressed(PlayerIndex.One))
            {
                effect.LightingEnabled = !effect.LightingEnabled;
                effect.DirectionalLight0.Enabled = true;
            }
        }

        private void DayNightToggle()
        {   
            if (input.isKeyPressed(Keys.N) || input.isButtonPressed(Buttons.X,PlayerIndex.One))
            {
                effect.LightingEnabled = !effect.LightingEnabled;

                effect.AmbientLightColor = Vector3.One * 0.2f;
                effect.DirectionalLight0.Enabled = false;
            }
        }


        private void UpdateCamera()
        {
            camRotation = Quaternion.Lerp(camRotation, charRotation, 0.2f);

            Vector3 camPos = character.camPos;
            camPos = Vector3.Transform(camPos, Matrix.CreateFromQuaternion(camRotation));
            camPos += character.charPos;

            Vector3 camUp = Vector3.Transform(Vector3.Up, Matrix.CreateFromQuaternion(camRotation));

            view = Matrix.CreateLookAt(camPos, character.charPos, Vector3.Up);

            projection = Matrix.CreatePerspectiveFieldOfView(fovAngle, aspectRatio, nearPlane, farPlane);
        }

        private void DoFog()
        {
            //enable fog
            if (input.isKeyPressed(Keys.R) || input.isButtonPressed(Buttons.LeftShoulder, PlayerIndex.One))            
                effect.FogEnabled = !effect.FogEnabled;
            if (effect.FogEnabled)
            {
                daySoundInstance.Volume = soundVolume/4;
                nightSoundInstance.Volume = soundVolume/2;
            }
            else
            {
                daySoundInstance.Volume = soundVolume;
                nightSoundInstance.Volume = soundVolume;
            }
        }

        private void DoZoom()
        {
            //zoom in and out
            if ((input.isKeyDown(Keys.LeftShift) && input.isKeyDown(Keys.Z)) || (input.isButtonDown(Buttons.B, PlayerIndex.One)))
                zoomFactor = 20;
            else
                if (input.isKeyDown(Keys.Z) || (input.isButtonDown(Buttons.A, PlayerIndex.One)))
                    zoomFactor = -20;
                else
                    zoomFactor = 0;
        }


        private void Move(float time)
        {
            isWalking = false;

            if (input.isKeyDown(Keys.LeftShift) || input.isButtonDown(Buttons.LeftTrigger, PlayerIndex.One))
            {
                movingSpeed = 0.15f;                
            }
            else
            {
                movingSpeed = 0.05f;
            }

            Vector3 leftRightVector = Vector3.Transform(Vector3.Right, charRotation);
            Vector3 fortBackVector = Vector3.Transform(Vector3.Forward, charRotation);
            

            if (isWallPenetrate)
            {
                if (input.isKeyDown(Keys.W))
                {
                    character.charPos += fortBackVector * movingSpeed;
                    isWalking = true;
                }
                if (input.isKeyDown(Keys.S))
                {
                    character.charPos -= fortBackVector * movingSpeed;
                    isWalking = true;
                }
                if (input.isKeyDown(Keys.A))
                {
                    character.charPos -= leftRightVector * movingSpeed;
                    isWalking = true;
                }
                if (input.isKeyDown(Keys.D))
                {
                    character.charPos += leftRightVector * movingSpeed;
                    isWalking = true;
                }

                character.charPos += movingSpeed * input.getLeftThumbstickValue(PlayerIndex.One).Y * fortBackVector;
                character.charPos += movingSpeed * input.getLeftThumbstickValue(PlayerIndex.One).X * leftRightVector;
            }
            else
            {                
                if (input.isKeyDown(Keys.W))
                {
                    character.setSphereCenter(character.charPos + fortBackVector * movingSpeed);
                    if (CollisionHandler.CheckColission(character.CamBorder, ref walls) == CollisionType.None)
                    {
                        character.charPos += fortBackVector * movingSpeed;
                    }
                    else
                    {
                        collideSound.Play();
                    }
                    character.setSphereCenter(character.charPos);
                    isWalking = true;
                }
                if (input.isKeyDown(Keys.S))
                {
                    character.setSphereCenter(character.charPos - fortBackVector * movingSpeed);
                    if (CollisionHandler.CheckColission(character.CamBorder, ref walls) == CollisionType.None)
                    {
                        character.charPos -= fortBackVector * movingSpeed;
                    }
                    else
                    {
                        collideSound.Play();
                    }
                    character.setSphereCenter(character.charPos);
                    isWalking = true;
                }
                if (input.isKeyDown(Keys.A))
                {
                    character.setSphereCenter(character.charPos - leftRightVector * movingSpeed);
                    if (CollisionHandler.CheckColission(character.CamBorder, ref walls) == CollisionType.None)
                    {
                        character.charPos -= leftRightVector * movingSpeed;
                    }
                    else
                    {
                        collideSound.Play();
                    }
                    character.setSphereCenter(character.charPos);
                    isWalking = true;
                }
                if (input.isKeyDown(Keys.D))
                {
                    character.setSphereCenter(character.charPos + leftRightVector * movingSpeed);
                    if (CollisionHandler.CheckColission(character.CamBorder, ref walls) == CollisionType.None)
                    {
                        character.charPos += leftRightVector * movingSpeed;
                    }
                    else
                    {
                        collideSound.Play();
                    }
                    character.setSphereCenter(character.charPos);
                    isWalking = true;
                }

                if(input.getLeftThumbstickValue(PlayerIndex.One) != new Vector2(0,0))                
                    isWalking = true;

                character.setSphereCenter(character.charPos + movingSpeed * input.getLeftThumbstickValue(PlayerIndex.One).Y * fortBackVector);
                if (CollisionHandler.CheckColission(character.CamBorder, ref walls) == CollisionType.None)
                {
                    character.charPos += movingSpeed * input.getLeftThumbstickValue(PlayerIndex.One).Y * fortBackVector;
                }
                else
                {
                    collideSound.Play();
                }
                character.setSphereCenter(character.charPos);

                character.setSphereCenter(character.charPos + movingSpeed * input.getLeftThumbstickValue(PlayerIndex.One).X * leftRightVector);
                if (CollisionHandler.CheckColission(character.CamBorder, ref walls) == CollisionType.None)
                {
                   
                    character.charPos += movingSpeed * input.getLeftThumbstickValue(PlayerIndex.One).X * leftRightVector;
                }
                else
                {
                    collideSound.Play();
                }
                character.setSphereCenter(character.charPos);
            }

            if (isWalking)            
                playerSoundInstance.Play();            
            else
                playerSoundInstance.Stop();
        }

        private void RotateUpDown(float time)
        {
            if (input.isKeyDown(Keys.Up))
            {
                upDownArc -= time;
            }

            if (input.isKeyDown(Keys.Down))
            {
                upDownArc += time;
            }

            upDownArc -= input.getRightThumbstickValue(PlayerIndex.One).Y * time;

            if (upDownArc > 90.0f)
                upDownArc = 90.0f;
            else if (upDownArc < -90.0f)
                upDownArc = -90.0f;
        }

        private void RotateLeftRight(float time)
        {
            float leftRightAngle = 0;

            if (input.isKeyDown(Keys.Left))
            {
                leftRightAngle += time;
            }
            if (input.isKeyDown(Keys.Right))
            {
                leftRightAngle -= time;
            }

            leftRightAngle -= input.getRightThumbstickValue(PlayerIndex.One).X * time;

            Quaternion additionalRot = Quaternion.CreateFromAxisAngle(Vector3.Up, leftRightAngle);// *Quaternion.CreateFromAxisAngle(Vector3.Right, upDownArc);
            charRotation *= additionalRot;

        }

        private void silence()
        {
            if (input.isKeyPressed(Keys.M) || input.isButtonPressed(Buttons.DPadLeft, PlayerIndex.One))
            {
                isSilence = !isSilence;
            }
            if (!isSilence)
            {
                if (!effect.LightingEnabled)
                {
                    daySoundInstance.Play();
                    nightSoundInstance.Stop();
                }
                else
                {
                    daySoundInstance.Stop();
                    nightSoundInstance.Play();
                }
            }
            else
            {
                daySoundInstance.Stop();
                nightSoundInstance.Stop();
            }
        }

        private void Reset()
        {
            if (input.isKeyPressed(Keys.Home) || input.isButtonPressed(Buttons.Start, PlayerIndex.One))
            {
                character.charPos = characterPositionDefault;

                upDownArc = 0;
                charRotation = Quaternion.Identity;

                //re-generate the maze
                maze.generateMaze();

                //clear the walls and floortiles
                walls.Clear();

                floorTiles.Clear();

                int i;
                //re-position walls and floortiles
                for (i = 0; i < maze.Row; i++)
                {
                    for (int j = 0; j < maze.Row; j++)
                    {
                        if (maze.Maze[i, j])
                        {
                            wallPos = new Vector3(i * wallSize.X, 0, -j * wallSize.Z);
                            walls.Add(new WallBlock(wallSize, wallPos));
                        }
                        else
                        {
                            tilePos = new Vector3(i * wallSize.X, 0, -j * wallSize.Z);
                            floorTiles.Add(new FloorTile(tileSize, tilePos));
                        }
                    }
                }

            }
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            //build floortiles and walls' shapes
            int i = 0;
            for (i = 0; i < floorTiles.Count; i++)
            {
                floorTiles.ElementAt(i).BuildShape();
            }
            for (i = 0; i < walls.Count; i++)
            {
                walls.ElementAt(i).BuildShape();
            }

            //set GraphicsDevice parameters to render correctly
            GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;

            //Draw floortiles first then walls later
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                effect.Texture = stageTextures[6];
                pass.Apply();
                for (int j = 0; j < floorTiles.Count; j++)
                {
                    GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, floorTiles[j].Faces[0], 0, floorTiles[j].PrimitivePerFace);
                }
                for (i = 0; i < 4; i++)
                {
                    effect.Texture = stageTextures[i];
                    pass.Apply();
                    for (int j = 0; j < walls.Count; j++)
                    {
                        GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, walls[j].Faces[i], 0, walls[j].PrimitivePerFace);
                    }
                }
                //bottom and top of the wall shouldn't have texture
                effect.Texture = null;
                pass.Apply();
                for (int j = 0; j < walls.Count; j++)
                {
                    GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, walls[j].Faces[4], 0, walls[j].PrimitivePerFace);
                    GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, walls[j].Faces[5], 0, walls[j].PrimitivePerFace);
                }
            }

            enemy.DrawModel(zombie, effect, 0, 0, 0);

            spriteBatch.Begin();

            spriteBatch.DrawString(font, distanceVolume.ToString(), new Vector2(23, 25), Color.Black);
            spriteBatch.DrawString(font, distanceVolume.ToString(), new Vector2(27, 25), Color.Black);
            spriteBatch.DrawString(font, distanceVolume.ToString(), new Vector2(25, 23), Color.Black);
            spriteBatch.DrawString(font, distanceVolume.ToString(), new Vector2(25, 27), Color.Black);
            spriteBatch.DrawString(font, distanceVolume.ToString(), new Vector2(25, 25), Color.Orange);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
